<?php
/**
 @prefix : <http://purl.org/net/ns/doas#> .
 @about: "<app/view/mmls/midi.php>", a: ":PHPScript",
 :shortdesc "Mabinogi MML string to MIDI format convertion script.";
 :author [:name "Logue"; :homepage <http://logue.be/> ];
 :license <http://www.gnu.org/licenses/gpl-3.0.html>;
 :acknowledgement [:name "Tamanegi"; homepage <http://www.annie.ne.jp/~tamanegi/> ];
 :dependencies "midi.class.php" .
*/
// Lorelei MML2MIDI Convertion Script
// Copyright (c)2008 Logue <http://logue.be/> All rights reserved.

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

// based on PSGConverter.php(http://www.annie.ne.jp/~tamanegi/)

include(APP.'vendors'.DS.'midi.class.php');	// Load PHP MIDI Class

$ch = 0;
$track = 0;
$mml_setting = array(); // テキストデータの初期化
$midiText = array();

// 環境設定用初期値の定義
// MIDIメッセージテキストの作成
if (isset($mmls)) {
	foreach ($mmls as $mml){
		$inst = $mml['Instrument']['program'];
		$isDram = $mml['Instrument']['isDram'];		// ドラムパートのフラグ兼楽器のノート値（0は通常。それ以外の場合ドラムパートのノート値）
		$pan = $mml['Mml']['pan']-1;	// パンポット
		$minNote = $mml['Instrument']['minNote'];	// 最小値
		$maxNote = $mml['Instrument']['maxNote'];	// 最大値

		if (!isset($mml['Mml']['name'])){
			$track_name = __($mml['Instrument']['name'],true);
		}else{
			$track_name = $mml['Mml']['name'];
		}

		$ch++;

		if($isDram == 0){
			$p_ch = $ch;
		}else{
			$p_ch = 10;	// ドラムパートの場合、チャンネル１０として処理。
			$pan = 64;
			// 処理をメロディ→和音１→和音２とする。
			$mml['Mml']['ch1'] = $mml['Mml']['ch1'].$mml['Mml']['ch2'];
			$mml['Mml']['ch2'] = '';
			$mml['Mml']['ch3'] = '';
		}
		
		if($mml['Mml']['ch1']) {
			$midiText[] = 'MTrk';
			if ($track == 0){
				$midiText[] = '0 SysEx f0 f0 7e 7f 09 01 f7';
				$midiText[] = '0 Meta TrkName "'. mb_convert_encoding($mml['Article']['title'],'SJIS','UTF-8') .'"';
				$midiText[] = '0 Meta Copyright "MML created by '. mb_convert_encoding($mml['User']['username'],'SJIS','UTF-8').' / Genelated by Lolerei(http://mabinogi.logue.be/)"';
			//	$midiText[] = '200 PrCh ch=10 p=46'; // 音色初期化
//				$modified = $mml['Article']['modified'];
			}
			// 初期設定
			$midiText[] = '0 Meta InstrName "'.mb_convert_encoding(sprintf('%s - %s',$track_name,__('Melody',true)),'SJIS','UTF-8').'"';
			$midiText[] = '192 PrCh ch='.$p_ch.' p='.$inst; // 音色初期化
			$midiText[] = '193 Par ch=' .$p_ch.' c=10 v='.$pan; // パンポット
			$midiText[] = '194 Par ch=' .$p_ch.' c=91 v=40'; // リバーブエフェクト
			$midiText[] = PSGConverter($mml['Mml']['ch1'],$p_ch,$isDram,$maxNote,$minNote);
			$midiText[] = 'TrkEnd';
			$track++;
		}
		if($mml['Mml']['ch2']) { 
			$midiText[] = 'MTrk';
			$midiText[] = '0 Meta InstrName "'.mb_convert_encoding(sprintf('%s - %s',$track_name,__('Chord 1',true)),'SJIS','UTF-8').'"';
			$midiText[] = '192 PrCh ch='.$p_ch.' p='.$inst; // 音色初期化
			$midiText[] = '193 Par ch=' .$p_ch.' c=10 v='.$pan; // パンポット
			$midiText[] = '194 Par ch=' .$p_ch.' c=91 v=40'; // リバーブエフェクト
			$midiText[] = PSGConverter($mml['Mml']['ch2'],$p_ch,$isDram,$maxNote,$minNote);
			$midiText[] = 'TrkEnd';
			$track++;
		}
		if($mml['Mml']['ch3']) {
			$midiText[] = 'MTrk';
			$midiText[] = '0 Meta InstrName "'.mb_convert_encoding(sprintf('%s - %s',$track_name,__('Chord 2',true)),'SJIS','UTF-8').'"';
			$midiText[] = '192 PrCh ch='.$p_ch.' p='.$inst; // 音色初期化
			$midiText[] = '193 Par ch=' .$p_ch.' c=10 v='.$pan; // パンポット
			$midiText[] = '194 Par ch=' .$p_ch.' c=91 v=40'; // リバーブエフェクト
			$midiText[] = PSGConverter($mml['Mml']['ch3'],$p_ch,$isDram,$maxNote,$minNote);
			$midiText[] = 'TrkEnd';
			$track++;
		}
	}
}else if (isset($mml)){
	$inst = $mml['Instrument']['program'];
	$isDram = $mml['Instrument']['isDram'];		// ドラムパートのフラグ兼楽器のノート値（0は通常。それ以外の場合ドラムパートのノート値）
	$pan = $mml['Mml']['pan']-1;	// パンポット
	$minNote = $mml['Instrument']['minNote'];	// 最小値
	$maxNote = $mml['Instrument']['maxNote'];	// 最大値

	if (!isset($mml['Mml']['name'])){
		$track_name = __($mml['Instrument']['name'],true);
	}else{
		$track_name = $mml['Mml']['name'];
	}

	$ch++;

	if($isDram == 0){
		$p_ch = $ch;
	}else{
		$p_ch = 10;	// ドラムパートの場合、チャンネル１０として処理。
		$pan = 64;
		$mml['Mml']['ch1'] = $mml['Mml']['ch1'].$mml['Mml']['ch2'];
		$mml['Mml']['ch2'] = '';
		$mml['Mml']['ch3'] = '';
	}

	if($mml['Mml']['ch1']) {
		$midiText[] = 'MTrk';
		if ($track == 0){
			$midiText[] = '0 SysEx f0 f0 7e 7f 09 01 f7';
			$midiText[] = '0 Meta TrkName "'. mb_convert_encoding($mml['Article']['title'],'SJIS','UTF-8') .'"';
			$midiText[] = '0 Meta Copyright "MML created by '. mb_convert_encoding($mml['User']['username'],'SJIS','UTF-8').' / Genelated by Lolerei(http://mabinogi.logue.be/)"';
		//	$midiText[] = '200 PrCh ch=10 p=46'; // 音色初期化
		}
		// 初期設定
		$midiText[] = '0 Meta InstrName "'.mb_convert_encoding(sprintf('%s - %s',$track_name,__('Melody',true)),'SJIS','UTF-8').'"';
		$midiText[] = '192 PrCh ch='.$p_ch.' p='.$inst; // 音色初期化
		$midiText[] = '193 Par ch=' .$p_ch.' c=10 v='.$pan; // パンポット
		$midiText[] = '194 Par ch=' .$p_ch.' c=91 v=40'; // リバーブエフェクト
		$midiText[] = PSGConverter($mml['Mml']['ch1'],$p_ch,$isDram,$maxNote,$minNote);
		$midiText[] = 'TrkEnd';
		$track++;
	}
	if($mml['Mml']['ch2']) { 
		$midiText[] = 'MTrk';
		$midiText[] = '0 Meta InstrName "'.mb_convert_encoding(sprintf('%s - %s',$track_name,__('Chord 1',true)),'SJIS','UTF-8').'"';
		$midiText[] = '192 PrCh ch='.$p_ch.' p='.$inst; // 音色初期化
		$midiText[] = '193 Par ch=' .$p_ch.' c=10 v='.$pan; // パンポット
		$midiText[] = '194 Par ch=' .$p_ch.' c=91 v=40'; // リバーブエフェクト
		$midiText[] = PSGConverter($mml['Mml']['ch2'],$p_ch,$isDram,$maxNote,$minNote);
		$midiText[] = 'TrkEnd';
		$track++;
	}
	if($mml['Mml']['ch3']) {
		$midiText[] = 'MTrk';
		$midiText[] = '0 Meta InstrName "'.mb_convert_encoding(sprintf('%s - %s',$track_name,__('Chord 2',true)),'SJIS','UTF-8').'"';
		$midiText[] = '192 PrCh ch='.$p_ch.' p='.$inst; // 音色初期化
		$midiText[] = '193 Par ch=' .$p_ch.' c=10 v='.$pan; // パンポット
		$midiText[] = '194 Par ch=' .$p_ch.' c=91 v=40'; // リバーブエフェクト
		$midiText[] = PSGConverter($mml['Mml']['ch3'],$p_ch,$isDram,$maxNote,$minNote);
		$midiText[] = 'TrkEnd';
		$track++;
	}
}
$output = 'MFile 1 '.$track.' 96'. "\n" . join("\n",$midiText);	// 楽譜データーをマージ

$midi=new Midi;
$midi->open();
$midi->importTxt($output); // MIDIメッセージテキスト読み込み
//$midi->convertToType0();	// SMF0に変換（QuickTime対策）

// MIDIクラスへのデータ読み込み・SMFファイルの作成
if (isset($debug)){
	header('Content-Type: text/plain');
	echo pr($output);
	Configure::write('debug', $debug);
}else{
	Configure::write('debug', 0);
	if (isset($modified) && !empty($modified)) {
		$modified = gmdate('D, d M Y H:i:s', strtotime($modified, time())) . ' GMT';
	} else {
		$modified = gmdate('D, d M Y H:i:s').' GMT';
	}
	$title = mb_convert_encoding($mmls['0']['Article']['title'],'SJIS','UTF-8');
	$buffer = $midi->getMid();
	header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
	header("Last-Modified: $modified");
	header('Content-Type: audio/midi');
	header('Content-Disposition: attachment; filename="'.$title.'.mml"');
	header('Content-Length: '.strlen($buffer));
	echo $buffer;
}
// メインここまで

/* PSGConverter本体　MML→MIDIテキスト */
function PSGConverter($mml,$ch,$isDram,$max,$min){
	$mml = ereg_replace("\r|\n","",$mml);
	// 命令の取得
	// $mmlを命令単位に分解して格納する

	// 1命令のデータ構造は
	//	([abcdefglnortvABCDEFGLNORTV<>]) 発音、環境設定
	//	([\+\#-]?) 調号
	//	([0-9]*) 入力値
	//	([\.]?) 補助記号
	//	(&?) 発音の延長
	// からなる。
	
	// 作業用変数の定義・初期化
	// 四分音符を96tickとする。2008/04/17のパッチ対応
	$cLength = 96; // = 1quarternote = 32tick
	$cOctave = 4;
	$cVolume = 8;
	$cNote = 0;			// "&"記号処理用
	$tieEnabled = 0; // "&"記号処理用


	$semibreve = $cLength*4;	// 1小節
	$minim = $cLength*2;	// １泊（Tick連動）
	$time = $semibreve;	// 先頭を１小節あける。

	preg_match_all('/[a-glnortvA-GLNORTV<>][\+\#-]?[0-9]*\.?&?/',$mml,$matches); // 入力されたMMLを命令単位で分割

	foreach($matches[0] as $mml_note) { // 1命令→メッセージ変換
		if (preg_match('/([lotvLOTV<>])([1-9][0-9]*|0?)(\.?)(&?)/',$mml_note,$RegExp)){
			//            $RegExp[1]    $RegExp[2]   $RegExp[3]
			//            コマンド名    値           オプション（タイや付点など）
			// ↑こんな風に代入される。

			$value = (int)$RegExp[2];	//値
			switch ($RegExp[1]){
				// 音長設定 Ln[.] (n=1～192)
				case 'l': case 'L':
					if((1<=$value) && ($value<=$minim)) {
						$cLength=floor($semibreve/$value);	// L1 -> 384tick .. L64 -> 6tick
						if($RegExp[3]==".") $cLength=floor($cLength*1.5); // 付点つき -> 1.5倍
						if($RegExp[4]=='&') $tieEnabled = 1;
					}
				break;

				// オクターブ設定 On (n=1～8)
				case 'O': case 'o':
					if ($isDram == 0) $cOctave = $value;
				break;

				// テンポ設定 Tn (n=32～255)
				case 'T': case 't':
					if( $value >= 32 && $value <= 255) {
						if ($value == 50){
							$cTempo = 1200000;	// テンポが50のときバグるので、決め打ち
						}else{
							$cTempo = floor(60000000 / $value);	// BPM -> microsec/quarternote
						}
						$TrackText[] = $time.' Tempo '.$cTempo; // テンポ設定
					}
				break;

				// ボリューム設定 Vn (n=0～15)
				case 'v': case 'V':
					if(0<=$value && $value<=15) $cVolume = $value; // ボリューム設定
				break;

				// 簡易オクターブ設定 {<>}
				case '<' :
					$cOctave=($cOctave<=1) ? 1 : ($cOctave-1);
				break;
				case '>' :
					$cOctave=($cOctave>=8) ? 8 : ($cOctave+1);
				break;

				// コメントアウトされていたら以降を処理しない（まきまびしーくに準ずる）
				case '//' :
					exit;
				break;
			}
			unset($RegExp);
		}

		// 音符設定 {ABCDEFG}[+#-][n][.][&] , Nn (n=1～64)
		if (preg_match('/([a-gnA-GN])([\+\#-]?)([0-9]*)(\.?)(&?)/',$mml_note,$RegExp)){
			$tick = $cLength; 
			$value = (int)$RegExp[3];
			switch ($RegExp[1]){
				case 'n': case 'N':
					// Nn, [A-G]数字なし -> Lで指定した長さに設定
					if((12<=$value) && ($value<=95)) $note=$value;
				break;
				default:
					// [A-G] 音名表記
					// 音符の長さ指定: n分音符→128分音符×tick数
					if(1<=$value && $value<=$minim) $tick=floor($semibreve / $value);	// L1 -> 384tick .. L64 -> 6tick
					if($RegExp[4]==".") $tick=floor($tick*1.5); // 付点つき -> 1.5倍

					if ($isDram == 0){
						// 音名→音階番号変換(C1 -> 12, C4 -> 48, ..)
						$note = (12 * $cOctave) + array_search(strtolower($RegExp[1]),array('c',2=>'d',4=>'e','f',7=>'g',9=>'a',11=>'b'));

						// 調音記号の処理
						switch($RegExp[2]){
							case '+':
							case '#':
								$note++;
							break;
							case '-':
								$note--;
							break;
						}
					}
				break;
			}

			if ($isDram == 0){
				// オクターブ調整（楽器の音域エミュレーション）
				while ($note < $min) $note = $note+12;
				while ($note > $max) $note = $note-12;
				$note += 12; // 1オクターブ低く演奏される不具合を修正 060426
			}else{
				// ドラムパートの場合ノートを強制的に指定
				$note = $isDram;
			}

			// ノートオン命令の追加
			if($tieEnabled == 1 && $note != $cNote) { // c&dなど無効なタイの処理
				$tieEnabled=0;
				$TrackText[] = $time.' Off ch='.$ch.' n='.$cNote.' v=100'; // 直前の音をOff
			}

			// 前回タイ記号が無いときのみノートオン
			if($tieEnabled == 0) $TrackText[] = $time.' On ch='.$ch.' n='.$note.' v='.(8*$cVolume);

			$time += $tick;				// タイムカウンタを音符の長さだけ進める

			// ノートオフ命令の追加
			if($RegExp[5]=='&') {	// タイ記号の処理
				$tieEnabled=1; 
				$cNote=$note; // 直前の音階を保存
			} else {
				$tieEnabled=0;
				$TrackText[] = $time.' Off ch='.$ch.' n='.$note.' v=100';
			}

		}else if($tieEnabled==1) {	// 無効なタイの処理
			$tieEnabled=0;
			$TrackText[] = $time.' Off ch='.$ch.' n='.$cNote.' v=100';	// 直前の音をOff
		}

		// 休符設定 R[n][.] (n=1～64)
		if (preg_match('/[rR]([0-9]*)(\.?)/',$mml_note,$RegExp)){
			$tick=$cLength; // 数字なし -> Lで指定した長さに設定
			$l=(int)$RegExp[1];
			if(1<=$l && $l<=$minim) $tick=floor($semibreve/$l);	// L1 -> 128tick .. L64 -> 2tick
			if($RegExp[2]==".") $tick=floor($tick*1.5);	// 付点つき -> 1.5倍
			$time += $tick;								// タイムカウンタを休符の長さだけ進める
		}

		unset ($RegExp);	// メモリ解放

	} // foreach // 1命令→メッセージ変換ここまで

	$time += $cLength; // 曲の最後に4分休符を挿入(Nexon仕様)

	// トラック終了命令
	$TrackText[] = $time.' Meta TrkEnd';
	return join("\n",$TrackText);
}
?>